﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Gallio.Framework;
using MbUnit.Framework;
using MbUnit.Framework.ContractVerifiers;
using PTP.Core;
using System.Threading;
using System.IO;
using PTP.Mock;
using PTP.TestUtil; //extension methods

namespace PTP.Devices
{
  public class MockDevice : GenericPTPDevice
  {
    public MockDevice(ContainerStream s) : base(s) { }

    public void BeginSendCommand(CommandBlockContainer command, bool setTransactionID)
    {
      this.beginSendCommand(command, setTransactionID, null);
    }

    public Container EndSendCommand(CommandBlockContainer command)
    {
      return this.endSendCommand(command, null);
    }
  }

  [TestFixture]
  public class PTPDeviceBaseTest
  {

    [Test]
    [Description("This is really an integration test for the whole stack. Uses pipe stream to transfer data")]
    //[Timeout(30)] //there was infinite loop
    public void GetDeviceInfo([BinaryData(ResourcePath = "GetDeviceInfo.bin", ResourceScope = typeof(PTP.Data.CanonEOS.Data_CanonEOS))] byte[] data)
    {
      using (ContainerStream cs = new ContainerStream(new MockPipeStream(data)))
      {
        GenericPTPDevice dev = new GenericPTPDevice(cs);
        DeviceInfo info = dev.GetDeviceInfo(); //Send GetDeviceInfo -command
        Assert.AreEqual<uint>(100, info.StandardVersion);
        Assert.AreEqual<uint>(6, info.VendorExtensionID);
        Assert.AreEqual<uint>(200, info.VendorExtensionVersion);
        Assert.AreEqual<string>(String.Empty, info.VendorExtensionDesc);
        Assert.AreEqual<uint>(0, info.FunctionalMode);
        //Assert.AreElementsEqual<uint>(..., info.OperationsSupported);
        //check only length
        Assert.AreEqual<int>(65, info.OperationsSupported.Length);
        Assert.AreElementsEqual<uint>(new uint[] { 0x4009, 0x4004, 0x4005, 0x4003, 0x4002, 0x4007, 0xC101 }, info.EventsSupported);
        Assert.AreElementsEqual<uint>(new uint[] { 0xD402, 0xD407, 0xD406 }, info.DevicePropertiesSupported);
        Assert.AreElementsEqual<uint>(new uint[] { 0x3801 }, info.CaptureFormats);
        Assert.AreElementsEqual<uint>(new uint[] { 0x3001, 0x3002, 0x3006, 0x300A, 0x3008, 0x3801, 0xB101, 0xB103, 0xBF02, 0x3800 }, info.ImageFormats);
        Assert.AreEqual<string>("Canon Inc.", info.Manufacturer);
        Assert.AreEqual<string>("Canon EOS 1000D", info.Model);
        Assert.AreEqual<string>("3-1.0.7", info.DeviceVersion);
        Assert.AreEqual<string>("ffffff67010f43ecbb7ff21ecc4a1111", info.SerialNumber);
      }
    }

    [Test]
    [Description("Send command using GenericPTPDevice and check that send bytes are right")]
    public void GetDeviceInfo_SentBytes([BinaryData(ResourcePath = "GetDeviceInfo.bin", ResourceScope = typeof(PTP.Data.CanonEOS.Data_CanonEOS))] byte[] data)
    {
      MockStream ms = new MockStream(data);
      using (ContainerStream cs = new ContainerStream(ms))
      {
        GenericPTPDevice dev = new GenericPTPDevice(cs);
        dev.GetDeviceInfo(); //Send GetDeviceInfo -command
        Assert.AreElementsEqual<byte>(
          // GetDeviceInfo = 0x1001
          //           |Container Length     | |Type     | |Code     | |TransactionID 
          new byte[] { 0x0C, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x10, 0x00, 0x00, 0x00, 0x00 },
          ms.GetSentData(true));
      }
    }

    [Test]
    [Description("Send command using GenericPTPDevice and check that send bytes are right")]
    //[Timeout(30)] //there was infinite loop
    //Code 0x2001 OK
    //                |Container Length     | |Type     | |Code     | |TransactionID        |
    [Row(new byte[] { 0x0C, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x20, 0x00, 0x00, 0x00, 0x00 })]
    public void OpenSession_SentBytes(byte[] data)
    {
      MockPipeStream ms = new MockPipeStream(data);
      using (ContainerStream cs = new ContainerStream(ms))
      {
        GenericPTPDevice dev = new GenericPTPDevice(cs);
        dev.OpenSession(5); //Send OpenSession -command
        Assert.AreElementsEqual<byte>(
          new CommandBlockContainer((int)CommandCode.OpenSession, 5).GetBytes(),
          ms.GetSentData(true));
      }
    }




    //TODO: The presence of TransactionID allows asynchronous events to refer to specific previously initiated operations.

    //TODO: If this field reaches its maximum value (0xFFFFFFFE), the device should "rollover" to 0x00000001.

    /*
USB Still Image Capture Device Definition:
The Still Image Bulk-only Protocol, which is based on the PIMA15740 protocol, does not
allow queuing of operations. Consequently when a Command Block that is optionally
followed by a Data Block is sent by the host on the OUT Stream, a Response Block
must be returned from the device on the IN Stream before the next Command Block can
be set by the host to the device on the OUT Stream.
    */

    public class CommandState
    {
      public CommandState(MockDevice testDevice) { this.TestDevice = testDevice; }
      public MockDevice TestDevice;
      public bool CommandSent = false;
      public ManualResetEvent Continue = new ManualResetEvent(false);
    }

    [Test]
    [Description("This test has a main thread and two other threads. Two threads send command at the same time. Only one should get the command through before reply arrives.")]
    public void CommandSynchronization()
    {
      //TODO: Fill response stream
      ContainerStream cs = new ContainerStream(new MockPipeStream(new ResponseBlockContainer(0x2001).GetBytes()));
      MockDevice testDevice = new MockDevice(cs);

      CommandState call1State = new CommandState(testDevice);
      CommandState call2State = new CommandState(testDevice);
      Exception innerThreadException = null;

      //Create identical work for threads
      ParameterizedThreadStart work = new ParameterizedThreadStart(delegate(object o)
      {
        try
        {
          CommandState state = (CommandState)o;
          //Start sending command
          CommandBlockContainer command = new CommandBlockContainer((int)CommandCode.SelfTest);
          state.TestDevice.BeginSendCommand(command, false);
          state.CommandSent = true;
          //pause
          state.Continue.WaitOne();
          //process response
          state.TestDevice.EndSendCommand(command);
        }
        catch (Exception ex)
        {
          innerThreadException = ex;
        }
      });
      Thread firstThread = new Thread(work);
      firstThread.Name = "First test thread";
      Thread secondThread = new Thread(work);
      secondThread.Name = "Second test thread";

      //Now threads start to execute
      firstThread.Start(call1State);
      secondThread.Start(call2State);
      for (int i = 0; i < 5; i++)
      {
        Thread.Sleep(200);
        const string msg = "Both threads shouldn't be able to send command";
        Assert.IsFalse(call1State.CommandSent && call2State.CommandSent, msg);
      }

      //check exceptions
      if (innerThreadException != null)
        throw new Exception("thread stopped to exception", innerThreadException);

      int calls = 0;
      if (call1State.CommandSent)
        calls++;
      if (call2State.CommandSent)
        calls++;
      Assert.AreEqual<int>(1, calls, "Number of calls didn't match");

      //let both threads to continue
      call1State.Continue.Set();
      call2State.Continue.Set();

      Thread.Sleep(500);
      calls = 0;
      if (call1State.CommandSent)
        calls++;
      if (call2State.CommandSent)
        calls++;
      Assert.AreEqual<int>(2, calls, "Number of calls didn't match");
      
    }


  }
}
